/************************************************************************************************************************
 * The input controller. Finger/Mouse abstraction
 * 
 ************************************************************************************************************************/

using UnityEngine;
using System.Collections.Generic;
using System;

public class InputController : MonoBehaviour
{

	#region Fields
	
	private const int MAX_FINGERS = 10;
	
	private Dictionary <int, List<ClickableObject>> interactions;
	
	
	
	// Event triggered when there is a mouse/touch event
	// Classes can subscribe to the following events to be notified
	public static event Action<Vector2, int> TouchBeganCallbackList;
	public static event Action<Vector2, int> TouchMovedCallbackList;
	public static event Action<Vector2, int> TouchStationaryCallbackList;
	public static event Action<Vector2, int> TouchEndedCallbackList;
	
	
	#endregion
	
	// begin test
	protected void TouchCallback1(Vector2 pos, int fingerID)
	{
		Debug.Log("TouchCallback1 is called, x = " + pos.x);
	}
	
	// end test
	
	#region Functions
	
	/// <summary>
	/// Instantiates the dictionary according to MAX_FINGERS
	/// checks all alwaysActiveInScene objects, if they have ClickableObject component
	/// else it will throw an IvalidArgumentException
	/// </summary>
	void Start()
	{
		interactions = new Dictionary<int,List<ClickableObject>>();
		for (int i = 0; i < MAX_FINGERS; ++i)
		{
			interactions.Add(i,new List<ClickableObject>());
		}
		
		// Set DPI for used device
		if(Screen.dpi == 0)
		{
			if (Application.isEditor)
			{
				DPI = PC_DPI;
			}
			else 
			{
				// DPI set in case some devices do not return anything in Screen.dpi
				DPI = DEFAULT_DPI;
			}
		}
		else DPI = Screen.dpi;
	}
	
	// Returns the Dots Per inch of the Screen, or default_dpi if it is impossible to retrieve the actual DPI
	// Calculate inches on a screen. Used for detecting Swiping
	private const float DEFAULT_DPI = 160.0f;
	private const float PC_DPI = 90f;
	
	public static float DPI { get; private set; }
	
	
	public float PixelsToInches(float distanceInPixels)
	{
		return distanceInPixels / DPI;
	}
	
	
	// Add a TouchMoved callback. Can be used for functions that only need to know when user moved the finger
	public void AddTouchMovedCallback(Action<Vector2, int> touchMovedCallback)
	{
		TouchMovedCallbackList += touchMovedCallback;
	}
	
	public void RemoveTouchMovedCallback(Action<Vector2, int> touchMovedCallback)
	{
		// TODO: does following line work even if touchMovedCallback is not a part of the TouchMovedCallbackList ?
		TouchMovedCallbackList -= touchMovedCallback;
	}
	
	
	// Add touch callbacks without a Stationary callback. Moved and Stationary will be treated as the same callback
	public void AddTouchCallbacks(Action<Vector2, int> touchBeganCallback, Action<Vector2, int> touchMovedCallback, Action<Vector2, int> touchEndedCallback)
	{
		AddTouchCallbacks (touchBeganCallback, touchMovedCallback, touchMovedCallback, touchEndedCallback);
	}
	
	public void AddTouchCallbacks(Action<Vector2, int> touchBeganCallback, Action<Vector2, int> touchMovedCallback,
									Action<Vector2, int> touchStationaryCallback, Action<Vector2, int> touchEndedCallback)
	{
		TouchBeganCallbackList += touchBeganCallback;
		TouchMovedCallbackList += touchMovedCallback;
		TouchStationaryCallbackList += touchStationaryCallback;
		TouchEndedCallbackList += touchEndedCallback; 
	}

	public void RemoveTouchCallbacks(Action<Vector2, int> touchBeganCallback, Action<Vector2, int> touchMovedCallback, Action<Vector2, int> touchEndedCallback)
	{
		RemoveTouchCallbacks (touchBeganCallback, touchMovedCallback, touchMovedCallback, touchEndedCallback);
	}
	
	public void RemoveTouchCallbacks(Action<Vector2, int> touchBeganCallback, Action<Vector2, int> touchMovedCallback,Action<Vector2, int> touchStationaryCallback, Action<Vector2, int> touchEndedCallback)
	{
		TouchBeganCallbackList -= touchBeganCallback;
		TouchMovedCallbackList -= touchMovedCallback;
		TouchStationaryCallbackList -= touchStationaryCallback;
		TouchEndedCallbackList -= touchEndedCallback;
	}

	
	/// <summary>
	/// Checks the phase of each Finger, which is beginning, moving or finishing
	/// It also fakes touch events for the mouse
	/// Furthermore it manages to quit the game in Android
	/// </summary>
	
	protected Vector2 lastTouchPosition;
	
	void Update()
	{
		// Create fake touch event for mouse
		if (Application.platform == RuntimePlatform.WindowsPlayer
			|| Application.platform == RuntimePlatform.WindowsEditor
			|| Application.platform == RuntimePlatform.OSXPlayer
			|| Application.platform == RuntimePlatform.OSXEditor)
		{
			if (Input.GetMouseButtonDown(0))
			{
				Began(Input.mousePosition, MAX_FINGERS - 1);
			}
			else if (Input.GetMouseButton(0))
			{
				// For compatibility with touch controls, we call Stationary() if the last touch position is equal to the current
				if(lastTouchPosition == (Vector2)Input.mousePosition)
				{
					Stationary(Input.mousePosition, MAX_FINGERS - 1);
				}
				else
				{
					Moved(Input.mousePosition, MAX_FINGERS - 1);
				}
				
				lastTouchPosition = Input.mousePosition;
			}
			else if (Input.GetMouseButtonUp(0))
			{
				Ended(Input.mousePosition, MAX_FINGERS - 1);
			}
		}
		// Assumes finger touchid's are ordered
		else
		{
			foreach(Touch touch in Input.touches)
			{
				if (touch.phase == TouchPhase.Began)
				{
					Began(touch.position, touch.fingerId);
				}
				
				else if (touch.phase == TouchPhase.Moved)
				{
					Moved(touch.position, touch.fingerId);
				}
				
				else if (touch.phase == TouchPhase.Stationary)
				{
					// TODO: implement a real stationary?
					// for now we just assume it has moved as well
					Stationary(touch.position, touch.fingerId);
				}
				
				else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
				{
					Ended(touch.position, touch.fingerId);
				}
			} // End foreach touch
			
			
			// Android hardware buttons
			if (Application.platform == RuntimePlatform.Android)
			{
				if (Input.GetKeyUp(KeyCode.Escape))
				{
					if( Application.loadedLevelName == "mainMenu")
					{
						Application.Quit();
					}
					else
					{
						Application.LoadLevel("mainMenu");
					}
				}
			}
		}
	}
	
	
	/// <summary>
	/// This Function collects all hits which are caused by a ray
	/// if the hit object is a ClickableObject, it will be collected and 
	/// its overriden function StartTouch will be called with the parameter hit.point 
	/// </summary>
	/// <param name="pos">
	/// A <see cref="Vector2"/>
	/// </param>
	/// <param name="id">
	/// A <see cref="System.Int32"/>
	/// </param>
	const float RayLength = 1000;
	
	private void Began(Vector2 pos, int id)
	{
		Ray ray;// = Camera.main.ScreenPointToRay(pos);
		RaycastHit hit;
		bool clickableObjectTouched = false;
		
		foreach (Camera camera in Camera.allCameras)
		{
			//Debug.Log("Check if we are hitting anything from camera: " + camera.name);

			ray = camera.ScreenPointToRay(pos);
			
			if (Physics.Raycast(ray, out hit, RayLength))
			{
				//Debug.Log("Touch Began, something hit: " + hit.transform.name);
				
				ClickableObject interaction = hit.transform.GetComponent<ClickableObject>();
				if (interaction)
				{
					interaction.BeganTouch(hit.point);
					interactions[id].Add(interaction);
					clickableObjectTouched = true;
				}
			}
		}
		
		if (! clickableObjectTouched) // no ClickableObjects selected, then we call the ClickCallback events
		{
			if(TouchBeganCallbackList != null)
			{
				//TouchBeganCallback.Invoke(pos);
				TouchBeganCallbackList(pos, id);
			}
			
		}
	}
	
	
	/// <summary> 
	/// /// Problem:
	/// /// This Function is not done yet.
	/// /// It is just updating the first hit and sends the hit's position to all MovedTouch
	/// /// Assumption: the connection between hit and gameobject is loose // maybe every racast it will generate new Raycasthit 
	/// 
	/// It checks all the positions of the casted rays and their hit points which are passed to MovedTouch
	/// </summary>
	/// <param name="pos">
	/// A <see cref="Vector2"/>
	/// </param>
	/// <param name="id">
	/// A <see cref="System.Int32"/>
	/// </param>
	private void Moved(Vector2 pos, int id)
	{
		// Calculate 3D point of finger on screen
		Ray ray = Camera.main.ScreenPointToRay(pos);
		bool clickableObjectTouched = false; // we only call ClickCallback events if no ClickableObject was selected
		
		if (interactions[id].Count > 0)
		{
			RaycastHit hit;

			if (Physics.Raycast(ray,out hit))
			{		
				foreach(ClickableObject interaction in interactions[id])
				{
					interaction.MovedTouch(hit.point);
					clickableObjectTouched = true;
				}

			}
		}
		
		if (! clickableObjectTouched) // no ClickableObjects selected, then we call the ClickCallback events
		{
			if(TouchMovedCallbackList != null)
			{
				//TouchBeganCallback.Invoke(pos);
				TouchMovedCallbackList(pos, id);
			}
			
		}
		
	}
	
	
	/// <summary> 
	///
	/// Just like Moved, except it calls the TouchStationaryCallbacks instead.
	///
	/// /// Problem:
	/// /// This Function is not done yet.
	/// /// It is just updating the first hit and sends the hit's position to all MovedTouch
	/// /// Assumption: the connection between hit and gameobject is loose // maybe every racast it will generate new Raycasthit 
	/// 
	/// It checks all the positions of the casted rays and their hit points which are passed to MovedTouch
	/// </summary>
	/// <param name="pos">
	/// A <see cref="Vector2"/>
	/// </param>
	/// <param name="id">
	/// A <see cref="System.Int32"/>
	/// </param>
	private void Stationary(Vector2 pos, int id)
	{
		// Calculate 3D point of finger on screen
		Ray ray = Camera.main.ScreenPointToRay(pos);
		bool clickableObjectTouched = false; // we only call ClickCallback events if no ClickableObject was selected
		
		if (interactions[id].Count > 0)
		{
			RaycastHit hit;

			if (Physics.Raycast(ray,out hit))
			{		
				foreach(ClickableObject interaction in interactions[id])
				{
					interaction.MovedTouch(hit.point);
					clickableObjectTouched = true;
				}

			}
		}
		
		if (! clickableObjectTouched) // no ClickableObjects selected, then we call the ClickCallback events
		{
			if(TouchStationaryCallbackList != null)
			{
				//TouchBeganCallback.Invoke(pos);
				TouchStationaryCallbackList(pos, id);
			}
			
		}
		
	}
	
	
	
	/// <summary>
	/// Tell a user interAction the finger has been released 
	/// (check for max fingers because of android error giving irratic finger IDs sometimes)
	/// </summary>
	/// <param name="id">
	/// A <see cref="System.Int32"/>
	/// </param> 
	private void Ended(Vector2 pos, int id)
	{
		//Debug.Log("END - finger ID: " + id + "\tX: " + pos.x + "; Y: " + pos.y);
		if (id < MAX_FINGERS && interactions[id].Count > 0)
		{
			foreach(ClickableObject interaction in interactions[id])
			{
				interaction.EndedTouch();
			}
			
			interactions[id].Clear();
		}
		
		else // no ClickableObjects selected, then we call the ClickCallback events
		{
			if(TouchEndedCallbackList != null)
			{
				//TouchBeganCallback.Invoke(pos);
				TouchEndedCallbackList(pos, id);
			}
			
		}
		
	}


	// This function returns angle in radians!
	public float GetPhoneAngle()
	{
		if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android)
		{
			return Mathf.Atan2(-Input.acceleration.x, Input.acceleration.y);
		}
		//else if (Application.platform == RuntimePlatform.Android)
		//{
		//	return Mathf.Atan2(Input.acceleration.y, Input.acceleration.x);
		//}
		else
		{
			return 0f; //mouseAngle;
		}
	}
	

#endregion

}
